package com.ubb.dbms.core.fileManagers.binaryFileManager;


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;


public abstract class BinaryFileIO {
	protected String fileName;
	protected String rsFileName;
	protected int headerLength = 0;
	protected int rowLength = 0;
	
	public BinaryFileIO() {
	}

	/**
	 * 
	 * @param fileName binary file name
	 * @param rsFileName row state file name of binary file (for deleted or available row)
	 * @param headerLength header length, can be 0 if no header need
	 * @param rowLength the length of one row in a table
	 */
	protected BinaryFileIO(String fileName, String rsFileName, int headerLength, int rowLength, boolean insertHeader) {
		this.fileName = fileName;
		this.rsFileName = rsFileName;
		this.headerLength = headerLength;
		this.rowLength = rowLength;
		handleCreateFile(fileName, insertHeader);
		handleCreateFile(rsFileName);
	}

	// create a binary file if not exists
	protected void handleCreateFile(String fileName) {
		this.handleCreateFile(fileName, false);
	}
	
	private void handleCreateFile(String fileName, boolean insertHeader) {
		File file = new File(fileName);
		if (!file.exists()) {
			System.out.println("Creating binary file: " + fileName);
			file.getParentFile().mkdirs();
			try {
				file.createNewFile();
				if (insertHeader) {
					this.handleUpdateHeader(new byte[headerLength]);
				}
			} catch (IOException e) {
				System.out.println("File not created: " + fileName);
			}
		}
	}

	protected void handleCreateDirectoryIfNotExists(String directoryName) {
		File directory = new File(directoryName);

		// if the directory does not exist, create it
		if (!directory.exists()) {
			System.out.println("Creating directory: " + directoryName);
			boolean result = directory.mkdir();
			if (result) {
				System.out
						.println("Directory created successfully. Directory name: "
								+ directoryName);
			} else {
				System.out.println("Directory not created! Directory name: "
						+ directoryName);
			}
		}
	}
	
	// change the header's value (the header contains only an offset now)
	protected void handleUpdateHeader(byte[] header) {
		FileOutputStream fileOutputStream = null;
		FileChannel fileChannel = null;
		try {
			try {
				fileOutputStream = new FileOutputStream(fileName, true);
				fileChannel = fileOutputStream.getChannel();
				fileChannel.write(ByteBuffer.wrap(header), 0);
			} finally {
				fileChannel.close();
				fileOutputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
	}

	protected byte[] handleReadFileHeader() {
		return handleReadRow(0, headerLength);
	}

	// insert a row to the binary file depending on the row state file
	protected int handleInsertRow(byte[] row) {
		int rowNumber = 0; // "row number" of deleted record (first row is 0) (used only if isDeleted == true)
		boolean isDeleted = false; // row is deleted or not
		
		// check if there is a deleted row in the binary file
		FileInputStream rowStateInputStream = null;
		try{
			try{
				rowStateInputStream = new FileInputStream(rsFileName);
				int rowState;
				while ((rowState = rowStateInputStream.read()) != -1) {
					if (rowState == 1) { // deleted row
						isDeleted = true;
						break;
					}
					rowNumber++;
				}
			} finally {
				rowStateInputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
		
		int offset = -1;
		
		// insert new row to the file and update the row state file too
		FileOutputStream fileOutputStream = null;
		FileChannel fileChannel = null;
		FileOutputStream rowStateOutputStream = null;
		FileChannel rowStateChannel = null;
		try{
			try{
				fileOutputStream = new FileOutputStream(fileName, true);
				rowStateOutputStream = new FileOutputStream(rsFileName, true);
				
				fileChannel = fileOutputStream.getChannel();
				rowStateChannel = rowStateOutputStream.getChannel();
				if (isDeleted) {
					
					offset = headerLength + rowNumber * rowLength;
					fileChannel.write(ByteBuffer.wrap(row), offset); // "update" a deleted row
					rowStateChannel.write(ByteBuffer.wrap(new byte[] {0}), rowNumber); // set row state to not deleted
				} else {
					offset = (int)fileChannel.size();
					fileChannel.position(offset);
					fileChannel.write(ByteBuffer.wrap(row)); // insert a row to the end of file
					rowStateChannel.position(rowStateChannel.size());
					rowStateChannel.write(ByteBuffer.wrap(new byte[] {0})); // insert a row state to the end of "row state file"
				}		
			} finally {
				fileChannel.close();
				fileOutputStream.close();
				rowStateChannel.close();
				rowStateOutputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
		return offset;
	}

	// change the correct byte in the row state file -> it means that row is deleted
	protected void handleDeleteRow(int offset) {
		FileOutputStream rowStateOutputStream = null;
		FileChannel rowStateChannel = null;
		try {
			try {
				rowStateOutputStream = new FileOutputStream(rsFileName, true);
				rowStateChannel = rowStateOutputStream.getChannel();
				int rowNumber = (offset - headerLength) / rowLength;
				rowStateChannel.write(ByteBuffer.wrap(new byte[] {1}), rowNumber);
			} finally {
				rowStateChannel.close();
				rowStateOutputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
	}
	
	// update a row in the binary file
	protected void handleUpdateRow(byte[] row, int offset) {
		FileOutputStream fileOutputStream = null;
		FileChannel fileChannel = null;
		try{
			try{
				fileOutputStream = new FileOutputStream(fileName, true);
				fileChannel = fileOutputStream.getChannel();
				fileChannel.write(ByteBuffer.wrap(row), offset); // "update" a row	
			} finally {
				fileChannel.close();
				fileOutputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
	}
	
	protected byte[] handleReadRow(int offset) {
		return this.handleReadRow(offset, rowLength);
	}
	
	protected byte[] handleReadRow(int offset, int rowLength) {
		return this.handleReadRow(offset, rowLength, fileName);
	}
	
	/**
	 * 
	 * @param offset an offset where a row is started in the file
	 * @param rowLength the "row length" in bytes that we want to read
	 * @return a row started from offset (the row length is rowLength) or null if something is incorrect  
	 */
	protected byte[] handleReadRow(int offset, int rowLength, String commonFileName) {	
		FileInputStream fileInputStream = null;
		try {
			try {
				fileInputStream = new FileInputStream(commonFileName);
				
				FileChannel ch = fileInputStream.getChannel();
				
				ByteBuffer byteBuffer = ByteBuffer.allocateDirect(rowLength);
				
				if (ch.read(byteBuffer, offset) == -1)
					return null;

				byteBuffer.position(0);
				byte[] row = new byte[rowLength];
				byteBuffer.get(row);
				
				return row;
				
			} finally {
				fileInputStream.close();
			}
		} catch (FileNotFoundException ex) {
			System.out.println("File not found.");
		} catch (IOException ex) {
			System.out.println(ex);
		}
		return null;
	}
	
	protected byte[] handleReadRowFromRsFile(int offset) {
		return this.handleReadRow(offset, 1, rsFileName);
	}
}
